﻿using System;
using System.Collections.Generic;

using Heaven.Graphics;
using Heaven.Texturing;

using Direct3D9 = SlimDX.Direct3D9;
using Slimdx = SlimDX;

namespace Heaven.SlimDX.Graphics
{
    /// <summary>
    /// Caches device representaions of texture objects
    /// </summary>
    public class TextureManager
    {
        #region Fields

        // Cache entity
        Dictionary<long, Direct3D9.BaseTexture> cache = new Dictionary<long, Direct3D9.BaseTexture>();
        // Cache for RenderTargets
        Dictionary<long, SlimDXRenderTarget> renderTargetCache = new Dictionary<long, SlimDXRenderTarget>();
        // Graphics gevice
        Direct3D9.Device device = null;

        // Empty 2D texture
        Direct3D9.Texture blankPlanarTexture = null;

        #endregion

        #region Properties

        /// <summary>
        /// Count of textures
        /// </summary>
        public int CountOfTextures
        {
            get { return cache.Count; }
        }

        /// <summary>
        /// Count of render targets
        /// </summary>
        public int CountOfRenderTargets
        {
            get { return renderTargetCache.Count; }
        }

        #endregion

        #region Initialization

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="device">Graphics device</param>
        public TextureManager(Direct3D9.Device device) 
        {
            this.device = device;
        }

        /// <summary>
        /// Destructor
        /// </summary>
        ~TextureManager()
        {
            Dispose();
        }

        /// <summary>
        /// Disposes unmanaged resources
        /// </summary>
        public void Dispose()
        {
            GC.SuppressFinalize(this);
            if (blankPlanarTexture != null)
            {
                blankPlanarTexture.Dispose();
            }
            foreach (Direct3D9.BaseTexture texture in cache.Values)
            {
                texture.Dispose();
            }
            cache.Clear();
            foreach (SlimDXRenderTarget renderTarget in renderTargetCache.Values)
            {
                renderTarget.Dispose();
            }
            renderTargetCache.Clear();
        }

        #endregion

        #region Methods

        /// <summary>
        /// Gets render target
        /// </summary>
        /// <param name="renderTarget">Heaven render target</param>
        /// <returns></returns>
        internal SlimDXRenderTarget GetRenderTarget(RenderTarget renderTarget)
        {
            SlimDXRenderTarget xnarendertarget = null;

            lock (renderTargetCache)
            {
                if (!renderTargetCache.TryGetValue(renderTarget.Hash, out xnarendertarget))
                {
                    xnarendertarget = CreateRenderTarget(renderTarget);
                    // Put into cache
                    renderTargetCache.Add(renderTarget.Hash, xnarendertarget);
                    // And when object will be collected by GC
                    renderTarget.GarbageCollected += new EventHandler(OnRenderTargetGarbageCollected);
                    renderTarget.DisposeRequested += new EventHandler(OnRenderTargetDisposeRequested);
                }
                return xnarendertarget;
            }
        }

        /// <summary>
        /// Gets SlimDX object for this Heaven texture
        /// </summary>
        /// <param name="texture">Texture object</param>
        public Direct3D9.BaseTexture Get(Texture texture)
        {
            Direct3D9.BaseTexture slimdxTexture = null;

            if(texture is RenderTarget)
            {
                lock (renderTargetCache)
                {
                    // If it is a render target
                    slimdxTexture = GetRenderTarget((RenderTarget)texture).SlimDXTexture;

                    if (slimdxTexture == null)
                    {
                        // Render target content is empty
                        return GetBlankPlanarTexture();
                    }
                    return slimdxTexture;
                }
            }

            lock (cache)
            {
                if (!cache.TryGetValue(texture.Hash, out slimdxTexture))
                {
                    // Create XNA texture
                    if (texture is PlanarTexture)
                    {
                        slimdxTexture = CreatePlanarTexture((PlanarTexture)texture);
                    }
                    else if (texture is CubicTexture)
                    {
                        slimdxTexture = CreateCubicTexture((CubicTexture)texture);
                    }
                    else if (texture is VolumeTexture)
                    {
                        slimdxTexture = CreateVolumeTexture((VolumeTexture)texture);
                    }

                    // Put into cache
                    cache.Add(texture.Hash, slimdxTexture);
                    // Subscibe event of changing mesh to remove it from cache
                    texture.Changed += new EventHandler(OnTextureChanged);
                    // And when object will be collected by GC
                    texture.GarbageCollected += new EventHandler(OnTextureGarbageCollected);
                }
            }

            return slimdxTexture;
        }

        Direct3D9.Texture GetBlankPlanarTexture()
        {
            if (blankPlanarTexture == null)
            {
                blankPlanarTexture = new Direct3D9.Texture(device, 2, 2, 1, Direct3D9.Usage.Dynamic, Direct3D9.Format.A8R8G8B8, Direct3D9.Pool.Default);
                Slimdx.DataRectangle data = blankPlanarTexture.LockRectangle(0, Direct3D9.LockFlags.None);
                data.Data.Write(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 0, 16);
                blankPlanarTexture.UnlockRectangle(0);
            }
            return blankPlanarTexture;
        }

        #endregion

        #region Event's handlers

        void OnTextureGarbageCollected(object sender, EventArgs e)
        {
            lock (cache)
            {
                // Remove texture from 
                // cache, so it is not valid now
                long hash = ((Texture)sender).Hash;
                Direct3D9.BaseTexture texture = null;
                if (cache.TryGetValue(hash, out texture))
                {
                    texture.Dispose();
                    cache.Remove(hash);
                }
            }
        }

        void OnRenderTargetGarbageCollected(object sender, EventArgs e)
        {
            // Remove render target from 
            // cache, so it is not valid now
            long hash = ((RenderTarget)sender).Hash;
            RemoveRenderTargetFromCache(hash);
        }
        
        void OnRenderTargetDisposeRequested(object sender, EventArgs e)
        {
            // Remove render target from 
            // cache, so it is not valid now
            long hash = ((RenderTarget)sender).Hash;
            RemoveRenderTargetFromCache(hash);
        }

        void RemoveRenderTargetFromCache(long hash)
        {
            lock (renderTargetCache)
            {
                SlimDXRenderTarget xnaRenderTarget = null;
                if (renderTargetCache.TryGetValue(hash, out xnaRenderTarget))
                {
                    renderTargetCache.Remove(hash);
                    xnaRenderTarget.Dispose();
                }
            }
        }

        void OnTextureChanged(object sender, EventArgs e)
        {
            lock (cache)
            {
                // Remove texture from 
                // cache, so it is not valid now
                long hash = ((Texture)sender).Hash;
                cache[hash].Dispose();
                cache.Remove(hash);
            }
        }

        #endregion

        #region Texture creation methods

        Direct3D9.Texture CreatePlanarTexture(PlanarTexture texture)
        {
            Direct3D9.Format format = Direct3D9.Format.Unknown;
            byte[] data = null;
            int bpp = 0;

            // Main layer
            GetTextureLayerData(texture.Layer, out data, out format);
            bpp = GetBytesPerPixel(format);

            Direct3D9.Texture texture2d = new Direct3D9.Texture(device, 
                texture.Layer.Width, texture.Layer.Height, 
                texture.Mipmaps.Length + 1, 
                Direct3D9.Usage.Dynamic, format, 
                Direct3D9.Pool.Default);
            Slimdx.DataRectangle dataStream = texture2d.LockRectangle(0, Direct3D9.LockFlags.None);
            dataStream.Data.Write(data, 0, data.Length);
            texture2d.UnlockRectangle(0);

            // Mipmaps
            for (int i = 0; i < texture.Mipmaps.Length; i++)
            {
                GetTextureLayerData(texture.Mipmaps[i], out data, out format);
                dataStream = texture2d.LockRectangle(i + 1, Direct3D9.LockFlags.None);
                dataStream.Data.Write(data, 0, data.Length);
                texture2d.UnlockRectangle(i+1);
            }
            return texture2d;
        }

        void GetTextureLayerData(Layer layer, out byte[] data, out Direct3D9.Format surfaceformat)
        {
            // Select format
            Direct3D9.Format format = SelectFormat(layer.Format);

            if (format == Direct3D9.Format.Unknown)
            {
                // Convert to default format if 
                // the given one's doesn't supported
                surfaceformat = Direct3D9.Format.A8R8G8B8;
                data = LayerFormats.A8R8G8B8.GetData(layer);                
            }
            else
            {
                data = layer.Data;
                surfaceformat = format;
            }
        }

        Direct3D9.CubeTexture CreateCubicTexture(CubicTexture texture)
        {
            // Get format of the CubeMap
            Direct3D9.Format format = SelectFormat(texture.NegativeX.Layer.Format);
            // Default format is Direct3D9.Format.A8R8G8B8
            if (format == Direct3D9.Format.Unknown) format = Direct3D9.Format.A8R8G8B8;

            Direct3D9.CubeTexture cubemap = new Direct3D9.CubeTexture(device, 512, texture.NegativeX.Mipmaps.Length + 1, Direct3D9.Usage.Dynamic, format, Direct3D9.Pool.Default);

            SetCubeMapFace(cubemap, Direct3D9.CubeMapFace.NegativeX, texture.NegativeX);
            SetCubeMapFace(cubemap, Direct3D9.CubeMapFace.NegativeY, texture.NegativeY);
            SetCubeMapFace(cubemap, Direct3D9.CubeMapFace.NegativeZ, texture.NegativeZ);
            SetCubeMapFace(cubemap, Direct3D9.CubeMapFace.PositiveX, texture.PositiveX);
            SetCubeMapFace(cubemap, Direct3D9.CubeMapFace.PositiveY, texture.PositiveY);
            SetCubeMapFace(cubemap, Direct3D9.CubeMapFace.PositiveZ, texture.PositiveZ);

            return cubemap;
        }

        Direct3D9.VolumeTexture CreateVolumeTexture(VolumeTexture texture)
        {
            Direct3D9.Format format = Direct3D9.Format.Unknown;
            byte[] data = null;
            int bpp = 0;

            // Main layer
            GetTextureLayerData(texture.Layer, out data, out format);
            bpp = GetBytesPerPixel(format);
            Direct3D9.VolumeTexture texture3d = new Direct3D9.VolumeTexture(device, texture.Width, texture.Height, texture.Depth, texture.Mipmaps.Length + 1, Direct3D9.Usage.Dynamic, format, Direct3D9.Pool.Default);
            Slimdx.DataBox dataStream = texture3d.LockBox(0, Direct3D9.LockFlags.None);
            dataStream.Data.Write(data, 0, data.Length);
            texture3d.UnlockBox(0);

            // Mipmaps
            for (int i = 0; i < texture.Mipmaps.Length; i++)
            {
                GetTextureLayerData(texture.Mipmaps[i], out data, out format);
                dataStream = texture3d.LockBox(i + 1, Direct3D9.LockFlags.None);
                dataStream.Data.Write(data, 0, data.Length);
                texture3d.UnlockBox(i + 1);
            }
            return texture3d;
        }

        SlimDXRenderTarget CreateRenderTarget(RenderTarget renderTarget)
        {
            Direct3D9.Format format = SelectFormat(renderTarget.Format);

            if (format == Direct3D9.Format.Unknown)
            {
                throw new ArgumentException("Render target doesn't support this format " + renderTarget.Format.ToString());
            }

            SlimDXRenderTarget xnarendertarget = new SlimDXRenderTarget(device, renderTarget.Width, renderTarget.Height, format);
            renderTarget.DataRequested += delegate 
            {
                xnarendertarget.GetData(renderTarget.Data);
            };

            return xnarendertarget;
        }

        Direct3D9.Format SelectFormat(LayerFormat layerFormat)
        {
            // Select format
            Direct3D9.Format format = Direct3D9.Format.Unknown;
            if (layerFormat == LayerFormats.A8R8G8B8) format = Direct3D9.Format.A8R8G8B8;
            if (layerFormat == LayerFormats.R32F) format = Direct3D9.Format.R32F;
            if (layerFormat == LayerFormats.A16B16G16R16F) format = Direct3D9.Format.A16B16G16R16F;

            return format;
        }

        void SetCubeMapFace(Direct3D9.CubeTexture cubemap, Direct3D9.CubeMapFace face, PlanarTexture texture)
        {
            Direct3D9.Format format = Direct3D9.Format.Unknown;
            byte[] data = null;
            int bpp = 0;

            // Main layer
            GetTextureLayerData(texture.Layer, out data, out format);
            bpp = GetBytesPerPixel(format);
            Slimdx.DataRectangle dataStream = cubemap.LockRectangle(face, 0, Direct3D9.LockFlags.None);
            dataStream.Data.Write(data, 0, data.Length);
            cubemap.UnlockRectangle(face, 0);            

            for (int i = 0; i < texture.Mipmaps.Length; i++)
            {
                GetTextureLayerData(texture.Mipmaps[i], out data, out format);
                dataStream = cubemap.LockRectangle(face, i+1, Direct3D9.LockFlags.None);
                dataStream.Data.Write(data, 0, data.Length);
                cubemap.UnlockRectangle(face, i+1);
            }
        }

        int GetBytesPerPixel(Direct3D9.Format format)
        {
            switch (format)
            {
                case Direct3D9.Format.A8R8G8B8: return 4;
            }

            throw new ArgumentException("There is no support getting BPP for " + format.ToString() + " in TextureManager");
        }

        #endregion
    }
}
